/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package fr.loof.fonzie.parsers;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import javax.annotation.PostConstruct;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.loof.fonzie.PersistenceQuery;
import fr.loof.fonzie.QueryParser;
import fr.loof.fonzie.keywords.Aggregate;
import fr.loof.fonzie.keywords.Between;
import fr.loof.fonzie.keywords.Empty;
import fr.loof.fonzie.keywords.Equals;
import fr.loof.fonzie.keywords.GreaterEquals;
import fr.loof.fonzie.keywords.GreaterThan;
import fr.loof.fonzie.keywords.In;
import fr.loof.fonzie.keywords.LessEquals;
import fr.loof.fonzie.keywords.LessThan;
import fr.loof.fonzie.keywords.Like;
import fr.loof.fonzie.keywords.NotEquals;
import fr.loof.fonzie.keywords.NotNull;
import fr.loof.fonzie.keywords.Null;

/**
 * @author <a href="nicolas@apache.org">Nicolas De Loof</a>
 */
public class JPQLQueryParser
    implements QueryParser
{
    private static Logger logger = LoggerFactory.getLogger( JPQLPersistenceQuery.class );

    private List<KeyWord> keywords = new LinkedList<KeyWord>();

    public PersistenceQuery parse( Class entityClass, Aggregate aggregate, String criteria, Class[] types )
    {
        logger.debug( "parse {} on {}", criteria, entityClass );
        String orderBy = "";
        int order = criteria.indexOf( "OrderBy" );
        if ( order >= 0 )
        {
            String clause = criteria.substring( order + "OrderBy".length() );
            criteria = criteria.substring( 0, order );
            orderBy = parseOrderClause( entityClass, clause );
        }

        JPQLPersistenceQuery jpqlQuery = new JPQLPersistenceQuery( entityClass, aggregate );
        if ( criteria.length() > 0 )
        {
            parseWhereClause( entityClass, criteria, jpqlQuery, types );
        }

        jpqlQuery.append( orderBy );
        return jpqlQuery;
    }

    @PostConstruct
    public JPQLQueryParser init()
    {
        keywords.add( new Empty() );
        keywords.add( new Equals() );
        keywords.add( new NotEquals() );
        keywords.add( new GreaterThan() );
        keywords.add( new LessThan() );
        keywords.add( new GreaterEquals() );
        keywords.add( new LessEquals() );
        keywords.add( new Null() );
        keywords.add( new NotNull() );
        keywords.add( new Between() );
        keywords.add( new Like() );
        keywords.add( new In() );

        Collections.sort( keywords );
        return this;
    }

    /**
     * @param entityClass
     * @param criteria
     */
    protected String parseOrderClause( Class entityClass, String criteria )
    {
        StringBuilder orderBy = new StringBuilder();
        String order = "";
        if ( criteria.endsWith( "Asc" ) )
        {
            criteria = criteria.substring( 0, criteria.length() - 3 );
            order = " asc";
        }
        if ( criteria.endsWith( "Desc" ) )
        {
            criteria = criteria.substring( 0, criteria.length() - 4 );
            order = " desc";
        }

        String[] properties = criteria.split( "And" );
        String separator = " order by ";
        for ( String property : properties )
        {
            property = StringUtils.uncapitalize( property );
            KeyWord.checkFieldExists( entityClass, property );
            orderBy.append( separator );
            separator = " and ";
            orderBy.append( "e." + property );
        }
        orderBy.append( order );
        return orderBy.toString();

    }

    @SuppressWarnings( "unchecked" )
    protected void parseWhereClause( Class entity, String criteria, JPQLPersistenceQuery query, Class[] types )
    {
        String[] constraints = criteria.split( "And" );
        String separator = " where ";

        List<Class> parameterTypes = new LinkedList<Class>();
        parameterTypes.addAll( Arrays.asList( types ) );

        for ( String constraint : constraints )
        {
            query.append( separator );
            separator = " and ";

            for ( KeyWord keyword : keywords )
            {
                if ( keyword.matches( query, entity, constraint, parameterTypes ) )
                {
                    break;
                }
            }
        }
        if ( parameterTypes.size() > 0 )
        {
            Class arg = parameterTypes.remove( 0 );
            if ( !arg.isAssignableFrom( int.class ) )
            {
                throw new IllegalStateException( "Max returned rows is not compatible with parameter type " + arg );
            }
        }
        if ( parameterTypes.size() > 0 )
        {
            Class arg = parameterTypes.remove( 0 );
            if ( !arg.isAssignableFrom( int.class ) )
            {
                throw new IllegalStateException( "First returned row is not compatible with parameter type " + arg );
            }
        }
        if ( parameterTypes.size() > 0 )
        {
            Class arg = parameterTypes.remove( 0 );
            throw new IllegalStateException( "Unused parameter of type " + arg );
        }
    }

    public void setCustomKeyWords( List<KeyWord> keyWords )
    {
        keyWords.addAll( keyWords );
    }
}
